package muhnes.sizoo.consulta;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.servlet.jsp.jstl.sql.Result;
import javax.servlet.jsp.jstl.sql.ResultSupport;
import javax.sql.DataSource;
import muhnes.sizoo.util.Asercion;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

public class ConsultaAvanzada implements Serializable {
    
    private List<Fuente> fuentes;
    private List<Campo> selectores;
    private List<Filtro> filtros;
    private List<Ordenador> ordenadores;

    public ConsultaAvanzada() {
        fuentes = new ArrayList<Fuente>();
        selectores = new ArrayList<Campo>();
        filtros = new ArrayList<Filtro>();
        ordenadores = new ArrayList<Ordenador>();
    }

    public ConsultaAvanzada(List<Fuente> fuentes, List<Campo> selectores, List<Filtro> filtros, List<Ordenador> ordenadores) {
        this.fuentes = fuentes;
        this.selectores = selectores;
        this.filtros = filtros;
        this.ordenadores = ordenadores;
    }

    public List<Filtro> getFiltros() {
        return filtros;
    }

    public void setFiltros(List<Filtro> filtros) {
        this.filtros = filtros;
    }

    public List<Fuente> getFuentes() {
        return fuentes;
    }

    public void setFuentes(List<Fuente> fuentes) {
        this.fuentes = fuentes;
    }

    public List<Ordenador> getOrdenadores() {
        return ordenadores;
    }

    public void setOrdenadores(List<Ordenador> ordenadores) {
        this.ordenadores = ordenadores;
    }

    public List<Campo> getSelectores() {
        return selectores;
    }

    public void setSelectores(List<Campo> selectores) {
        this.selectores = selectores;
    }
    
    public Fuente getFuentePorId( String id ) {
        Fuente fuente = null;
        for( Fuente f : fuentes ) {
            if( f.getId().equals(id) ) {
                fuente = f;
                break;
            }
        }
        return fuente;
    }
    
    public Campo getCampoPorId( String id ) {
        for( Fuente fuente : fuentes ) {
            for( Campo campo : fuente.getCampos() ) {
                if( campo.getId().equals(id) ) {
                    return campo;
                }
            }
        }
        return null;
    }
    
    public Campo getCampoPorValor( Fuente fuente, String valor ) {
        Campo campo = null;
        for( Campo c : fuente.getCampos() ) {
            if( c.getValor().equals(valor) ) {
                campo = c;
                break;
            }
        }
        return campo;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ConsultaAvanzada other = (ConsultaAvanzada) obj;
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        return hash;
    }
    
    public static ConsultaAvanzada cargarDefinicion( String ruta )  
            throws DocumentException, SQLException, Exception {
        ConsultaAvanzada consulta = new ConsultaAvanzada();
        
        SAXReader lector = new SAXReader();
        Document documento = lector.read(ruta);
        Element elementoFuentes = documento.getRootElement().element("fuentes");

        for ( Iterator elementosFuente = elementoFuentes.elementIterator("fuente"); elementosFuente.hasNext(); ) {
            Element elementoFuente = (Element)elementosFuente.next();
            
            String idFuente = null;
            String valorFuente = null;
            String nombreFuente = null;
            String descripcionFuente = "";
            TipoFuente tipoFuente = TipoFuente.INDEFINIDO;
            Map<String,Fuente> referencias = null;
            
            Asercion.verificar(
                elementoFuente.attribute("id") != null, 
                "El atributo id no esta definido para la fuente"
            );
            idFuente = elementoFuente.attribute("id").getValue();
            
            Asercion.verificar(
                elementoFuente.attribute("valor") != null, 
                "El atributo valor no esta definido para la fuente " + idFuente
            );
            valorFuente = elementoFuente.attribute("valor").getValue();
            
            Asercion.verificar(
                elementoFuente.attribute("nombre") != null,
                "El atributo nombre no esta definido para la fuente " + idFuente
            );
            nombreFuente = elementoFuente.attribute("nombre").getValue();
            
            Asercion.verificar(
                elementoFuente.attribute("tipo") != null,
                "El atributo tipo no esta definido para la fuente " + idFuente
            );
            tipoFuente = TipoFuente.valueOf(elementoFuente.attribute("tipo").getValue().toUpperCase());
            
            if( elementoFuente.element("descripcion") != null ) {
                descripcionFuente = elementoFuente.element("descripcion").getStringValue();
            }
            
            Fuente fuente = new Fuente( idFuente, valorFuente, nombreFuente, descripcionFuente, tipoFuente );
            
            for ( Iterator elementosCampo = elementoFuente.elementIterator("campo"); elementosCampo.hasNext(); ) {
                Element elementoCampo = (Element)elementosCampo.next();
                
                String idCampo = null;
                String valorCampo = null;
                String nombreCampo = null;
                String descripcionCampo = "";
                TipoDato tipoDato = TipoDato.INDEFINIDO;
                boolean multivalorado = false;
                Map<String,String> opciones = null;
                
                Asercion.verificar(
                    elementoCampo.attribute("id") != null,
                    "El atributo id no esta definido para el campo"
                );
                idCampo = elementoCampo.attribute("id").getValue();
                
                Asercion.verificar(
                    elementoCampo.attribute("valor") != null,
                    "El atributo valor no esta definido para el campo " + idCampo
                );
                valorCampo = elementoCampo.attribute("valor").getValue();

                Asercion.verificar(
                    elementoCampo.attribute("nombre") != null,
                    "El atributo nombre no esta definido para el campo " + idCampo
                );
                nombreCampo = elementoCampo.attribute("nombre").getValue();

                Asercion.verificar(
                    elementoCampo.attribute("tipo") != null,
                    "El atributo tipo no esta definido para el campo " + idCampo
                );
                tipoDato = TipoDato.valueOf(elementoCampo.attribute("tipo").getValue().toUpperCase());
                
                if( elementoCampo.attribute("multiple") != null ) {
                    multivalorado = Boolean.valueOf(elementoCampo.attribute("multiple").getValue());
                }
                
                if( elementoCampo.element("opciones") != null ) {
                    opciones = obtenerOpciones( elementoCampo.element("opciones") );
                }
                
                if( elementoCampo.element("descripcion") != null ) {
                    descripcionCampo = elementoCampo.element("descripcion").getStringValue();
                }
                
                Campo campo = new Campo( idCampo, valorCampo, nombreCampo, descripcionCampo, tipoDato, multivalorado, opciones, fuente );
                fuente.getCampos().add(campo);
                
            }
            
            if( elementoFuente.element("referencias") != null ) {
                for ( Iterator elementosReferencia = elementoFuente.element("referencias").elementIterator("referencia"); elementosReferencia.hasNext(); ) {
                    Element elementoReferencia = (Element)elementosReferencia.next();

                    String campoReferencia = null;
                    String fuenteReferencia = null;
                    String vinculoReferencia = null;

                    Asercion.verificar(
                        elementoReferencia.attribute("campo") != null,
                        "El atributo campo no esta definido para la referencia en " + idFuente
                    );
                    campoReferencia = elementoReferencia.attribute("campo").getValue();

                    Asercion.verificar(
                        elementoReferencia.attribute("fuente") != null,
                        "El atributo fuente no esta definido para la referencia en " + idFuente
                    );
                    fuenteReferencia = elementoReferencia.attribute("fuente").getValue();

                    Asercion.verificar(
                        elementoReferencia.attribute("vinculo") != null,
                        "El atributo vinculo no esta definido para la referencia en " + idFuente
                    );
                    vinculoReferencia = elementoReferencia.attribute("vinculo").getValue();

                    Referencia referencia = new Referencia(campoReferencia,fuenteReferencia,vinculoReferencia);
                    fuente.getReferencias().put(fuenteReferencia, referencia);

                }
            }
            
            consulta.getFuentes().add(fuente);
            
        }
        
        return consulta;
    }
    
    public static Map<String,String> obtenerOpciones( Element elementoOpciones ) throws SQLException, Exception {
        Map<String,String> opciones = new HashMap<String,String>();
        if( elementoOpciones.attribute("consulta") != null ) {
            Context ctx = new InitialContext();
	    DataSource fuenteJndi = (DataSource)ctx.lookup("jdbc/Sizoo");
            
            Asercion.verificar(
                fuenteJndi != null,
                "El recurso JNDI no puede ser obtenido"
            );
 
            Connection conexion = fuenteJndi.getConnection();
 
            Asercion.verificar(
                conexion != null,
                "La conexión no puede establecida"
            );
 
            String consulta = elementoOpciones.attribute("consulta").getValue();
	    PreparedStatement sentencia = conexion.prepareStatement(consulta); 
            ResultSet conjunto =  sentencia.executeQuery();
            Result resultado = ResultSupport.toResult(conjunto);
            
            String valor = null;
            String etiqueta = null;
            Element elementoOpcion = elementoOpciones.element("opcion");
            
            Asercion.verificar(
                elementoOpcion != null,
                "Elemento opcion no definido para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            
            Asercion.verificar(
                elementoOpcion.attribute("valor") != null,
                "El atributo valor no esta definido para el elemento opcion para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            valor = elementoOpcion.attribute("valor").getValue();
            
            Asercion.verificar(
                elementoOpcion.attribute("etiqueta") != null,
                "El atributo etiqueta no esta definido para el elemento opcion para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            etiqueta = elementoOpcion.attribute("etiqueta").getValue();    
                    
            Asercion.verificar(
                resultado.getRowCount() > 0,
                "No hay valores para obtener las opciones para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            
            Asercion.verificar(
                Arrays.asList(resultado.getColumnNames()).contains(valor),
                "La consulta para las opciones no contiene la columna " + valor + " para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            
            Asercion.verificar(
                Arrays.asList(resultado.getColumnNames()).contains(etiqueta),
                "La consulta para las opciones no contiene la columna " + etiqueta + " para el campo " + elementoOpciones.getParent().attributeValue("id")
            );
            
            for( SortedMap registro : resultado.getRows() ) {
                if( registro.get(valor) != null && registro.get(etiqueta) != null ) {
                    opciones.put( 
                        registro.get(valor).toString(), 
                        registro.get(etiqueta).toString()
                    );
                }
            }
            
            if( !conexion.isClosed() ) {
                conexion.close();
            }
            
        }
        else {
            for ( Iterator elementosOpcion = elementoOpciones.elementIterator("opcion"); elementosOpcion.hasNext(); ) {
                Element elementoOpcion = (Element)elementosOpcion.next();
                String valor = null;
                String etiqueta = null;
                
                Asercion.verificar(
                    elementoOpcion.attribute("valor") != null,
                    "El atributo valor no esta definido para el elemento opcion"
                );
                valor = elementoOpcion.attribute("valor").getValue();
                
                Asercion.verificar(
                    elementoOpcion.attribute("etiqueta") != null,
                    "El atributo etiqueta no esta definido para el elemento opcion"
                );
                etiqueta = elementoOpcion.attribute("etiqueta").getValue();
                
                opciones.put(valor, etiqueta);
            }
        }
        
        return opciones;
    }
    
    public String crearConsultaSQL() {
        String consulta = "";
        String fragmentoCampos = "";
        String fragmentoFuentes = "";
        String fragmentoFiltros = "";
        String fragmentoOrdenadores = "";
        
        for( Campo campo : selectores ) {
            fragmentoCampos += campo.getValorConsultable() + ", ";
        }
        fragmentoCampos = StringUtils.strip(fragmentoCampos, ", ");
        
        List<String> fuentesSeleccionadas = new ArrayList<String>();
        for( Campo campo : selectores ) {
            if( !fuentesSeleccionadas.contains( campo.getFuente().getId() ) ) {
                fuentesSeleccionadas.add( campo.getFuente().getId() );
                fragmentoFuentes += campo.getFuente().getValorConsultable() + ", ";
            }
        }
        for( Filtro filtro : filtros ) {
            if( !fuentesSeleccionadas.contains( filtro.getCampo().getFuente().getId() ) ) {
                fuentesSeleccionadas.add( filtro.getCampo().getFuente().getId() );
                fragmentoFuentes += filtro.getCampo().getFuente().getValorConsultable() + ", ";
            }
        }
        fragmentoFuentes = StringUtils.strip(fragmentoFuentes, ", ");
        
        for( Filtro filtro : filtros ) {
            String operador = filtro.isRequerido() ? "\nAND " : "\nOR ";
            fragmentoFiltros += operador + filtro.getValorConsultable();
        }
        
        Fuente fuente1 = getFuentePorId(fuentesSeleccionadas.get(0));
        for( int i = 1; i < fuentesSeleccionadas.size(); ++i ) {
            Fuente fuente2 = getFuentePorId(fuentesSeleccionadas.get(i));
            Referencia referencia = fuente1.getReferencias().get( fuente2.getId() );
            fragmentoFiltros += "\nAND " + fuente1.getValorConsultable() + 
                                "." +
                                referencia.getCampoLocal() + 
                                " = " +
                                fuente2.getValorConsultable() + 
                                "." +
                                referencia.getCampoReferenciado();
            fuente1 = fuente2;
        }
        
        fragmentoFiltros = StringUtils.strip(fragmentoFiltros, "\nAND ");
        fragmentoFiltros = StringUtils.strip(fragmentoFiltros, "\nOR ");
        
        for( Ordenador ordenador : ordenadores ) {
            fragmentoOrdenadores += ordenador.getValorConsultable() + ", ";
        }
        fragmentoOrdenadores = StringUtils.strip(fragmentoOrdenadores, ", ");
        
        consulta = "SELECT " + fragmentoCampos + "\nFROM " + fragmentoFuentes;
        consulta += fragmentoFiltros.isEmpty() ? "" : "\nWHERE " + fragmentoFiltros;
        consulta += fragmentoOrdenadores.isEmpty() ? "" : "\nORDER BY " + fragmentoOrdenadores;
        
        System.out.println(consulta);
        return consulta;
    }
    
    public String crearConsultaLegible() {
        String consulta = "";
        String fragmentoCampos = "";
        String fragmentoFiltros = "";
        String fragmentoOrdenadores = "";
        
        for( Campo campo : selectores ) {
            fragmentoCampos += campo.getNombre() + ", ";
        }
        fragmentoCampos = StringUtils.strip(fragmentoCampos, ", ");
        
        for( Filtro filtro : filtros ) {
            Campo campo = filtro.getCampo();
            String valores;
            if( campo.isMultivalorado() ) {
                List<String> v = new ArrayList<String>();
                for(String valor : StringUtils.split(filtro.getValor(),",")) {
                    v.add(campo.isSeleccionable() ?
                          campo.getOpciones().get(valor) :
                          valor);
                }
                valores = StringUtils.join(v,",");
            }
            else {
                valores = campo.isSeleccionable() ?
                          campo.getOpciones().get(filtro.getValor()) :
                          filtro.getValor();
            }
            
            
            fragmentoFiltros += "<br/>" + filtro.getCampo().getNombre() +
                    " " + Filtro.etiquetaComparaciones.get(filtro.getComparacion()).toLowerCase() +
                    " " + valores.toLowerCase();
        }
        
        for( Ordenador ordenador : ordenadores ) {
            fragmentoOrdenadores += "<br/>" + ordenador.getCampo().getNombre() 
                    + " orden "
                    + " " + ordenador.getOrdenacion().toString().toLowerCase();
        }
        
        consulta = "<b>Campos seleccionados:</b><br/>" + fragmentoCampos;
        consulta += fragmentoFiltros.isEmpty() ? "" : "<br/><b>Filtrado por:</b>" + fragmentoFiltros;
        consulta += fragmentoOrdenadores.isEmpty() ? "" : "<br/><b>Ordenado por:</b>" + fragmentoOrdenadores;
        
        return consulta;
    }
    
}
